Nhóm sử dụng tập dữ liệu chứa thông tin giao dịch của khách hàng từ 10 trung tâm mua sắm lớn tại đất nước Istanbul, từ năm 2021 đến thời điểm hiện tại năm 2023 trên Kaggle. Ngoài thông tin giao dịch, tập dữ liệu cũng cung cấp thông tin về độ tuổi, giới tính, phù hợp với nghiệp vụ khai phá.
import matplotlib.pyplot as plt
import pandas as pd
transactions = pd.read_csv('data/transactions.csv')
transactions.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 99457 entries, 0 to 99456 Data columns (total 10 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 invoice_no 99457 non-null object 1 customer_id 99457 non-null object 2 gender 99457 non-null object 3 age 99457 non-null int64 4 category 99457 non-null object 5 quantity 99457 non-null int64 6 price 99457 non-null float64 7 payment_method 99457 non-null object 8 invoice_date 99457 non-null object 9 shopping_mall 99457 non-null object dtypes: float64(1), int64(2), object(7) memory usage: 7.6+ MB
Tập dữ liệu có 99457 giao dịch và 10 cột.
| Attribute | Description | Example | Data type |
|---|---|---|---|
| invoice_no | Mã giao dịch | I138884 | Categorical |
| customer_id | Mã khách hàng | C241288 | Categorical |
| gender | Giới tính | Male, Female | Categorical |
| age | Độ tuổi | 18, 69 | Numerical |
| category | Danh mục sản phẩm | Clothing | Categorical |
| quantity | Số lượng sản phẩm trong giao dịch | 1, 5 | Numerical |
| price | Đơn giá sản phẩm trong giao dịch | 1500.4 | Numerical |
| payment_method | Phương thức thanh toán | Cash, Credit Card, Debit Card | Categorical |
| invoice_date | Ngày diễn ra giao dịch | 5/8/2022 | Categorical |
| shopping_mall | Địa điểm diễn ra giao dịch | Kanyon | Categorical |
transactions.sample(5)
| invoice_no | customer_id | gender | age | category | quantity | price | payment_method | invoice_date | shopping_mall | |
|---|---|---|---|---|---|---|---|---|---|---|
| 92256 | I654927 | C308067 | Male | 23 | Toys | 3 | 107.52 | Cash | 15/01/2023 | Kanyon |
| 84153 | I321680 | C453411 | Female | 61 | Cosmetics | 2 | 81.32 | Credit Card | 12/1/2022 | Metrocity |
| 31812 | I208020 | C126610 | Female | 68 | Clothing | 2 | 600.16 | Credit Card | 6/6/2021 | Kanyon |
| 17405 | I101596 | C309263 | Female | 43 | Books | 5 | 75.75 | Cash | 30/08/2021 | Kanyon |
| 33833 | I167707 | C961101 | Male | 35 | Clothing | 1 | 300.08 | Cash | 8/12/2022 | Metropol AVM |
transactions.isnull().sum()
invoice_no 0 customer_id 0 gender 0 age 0 category 0 quantity 0 price 0 payment_method 0 invoice_date 0 shopping_mall 0 dtype: int64
transactions.duplicated().sum()
0
Tập dữ liệu không chứa giá trị null ở bất kỳ cột nào và không có giao dịch trùng lặp.
Để phục vụ việc khai phá về sau, nhóm sẽ tạo cột mới chứa thông tin tổng số tiền thanh toán trên mỗi giao dịch.
transactions['total'] = transactions['quantity'] * transactions['price']
transactions.sample(5)
| invoice_no | customer_id | gender | age | category | quantity | price | payment_method | invoice_date | shopping_mall | total | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 41358 | I131628 | C323483 | Male | 53 | Toys | 1 | 35.84 | Cash | 13/01/2022 | Metrocity | 35.84 |
| 29627 | I134426 | C127925 | Male | 47 | Cosmetics | 4 | 162.64 | Credit Card | 13/04/2022 | Kanyon | 650.56 |
| 30530 | I225225 | C511061 | Male | 36 | Cosmetics | 5 | 203.30 | Cash | 4/12/2021 | Emaar Square Mall | 1016.50 |
| 81535 | I306915 | C184292 | Male | 65 | Souvenir | 5 | 58.65 | Cash | 19/09/2021 | Istinye Park | 293.25 |
| 73970 | I900498 | C828029 | Male | 50 | Clothing | 3 | 900.24 | Credit Card | 19/02/2023 | Kanyon | 2700.72 |
Nhóm cũng sẽ thực hiện nhóm tuổi khách hàng thành 6 độ tuổi để giảm độ nhiễu của tập dữ liệu: 18 đến 24, 25 đến 34, 35 đến 44, 45 đến 54, 55 đến 64, và 65 đến 70.
bins = [18, 24, 34, 44, 54, 64, 70]
labels = ['18-24', '25-34', '35-44', '45-54', '55-64', '65-70']
transactions['age_group'] = pd.cut(transactions['age'], bins=bins, labels=labels)
age_group_type = pd.CategoricalDtype(labels, ordered=True)
transactions['age_group'] = transactions['age_group'].astype(age_group_type)
transactions.drop('age', axis=1, inplace=True)
transactions.sample(5)
| invoice_no | customer_id | gender | category | quantity | price | payment_method | invoice_date | shopping_mall | total | age_group | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 8558 | I701549 | C939167 | Female | Shoes | 1 | 600.17 | Credit Card | 24/09/2022 | Zorlu Center | 600.17 | 45-54 |
| 80651 | I322320 | C298578 | Female | Food and Beverage | 4 | 20.92 | Debit Card | 14/05/2021 | Mall of Istanbul | 83.68 | 45-54 |
| 83457 | I614717 | C800700 | Male | Technology | 5 | 5250.00 | Debit Card | 4/8/2021 | Kanyon | 26250.00 | 45-54 |
| 28625 | I252146 | C992348 | Female | Cosmetics | 3 | 121.98 | Credit Card | 20/11/2022 | Mall of Istanbul | 365.94 | 55-64 |
| 87439 | I394044 | C108674 | Female | Souvenir | 4 | 46.92 | Debit Card | 27/02/2021 | Kanyon | 187.68 | 55-64 |
Nhóm có thể giảm lượng dữ liệu qua việc loại bỏ cột không mang ý nghĩa khai phá như mã giao dịch và mã khách hàng.
transactions.duplicated(subset=['invoice_no']).any()
False
transactions.duplicated(subset=['customer_id']).any()
False
Tập dữ liệu không có giao dịch với cùng mã giao dịch hoặc cùng mã khách hàng. Điều này có nghĩa mỗi khách hàng chỉ thực hiện giao dịch một lần. Vì vậy, nhóm có thể loại bỏ hai cột này.
transactions.drop(['invoice_no', 'customer_id'], axis=1, inplace=True)
transactions.sample(5)
| gender | category | quantity | price | payment_method | invoice_date | shopping_mall | total | age_group | |
|---|---|---|---|---|---|---|---|---|---|
| 22513 | Female | Toys | 3 | 107.52 | Credit Card | 14/08/2022 | Metropol AVM | 322.56 | 55-64 |
| 1473 | Male | Toys | 5 | 179.20 | Cash | 22/09/2022 | Metrocity | 896.00 | 35-44 |
| 14754 | Male | Shoes | 3 | 1800.51 | Cash | 12/6/2021 | Metrocity | 5401.53 | 18-24 |
| 56969 | Male | Shoes | 4 | 2400.68 | Cash | 20/01/2021 | Kanyon | 9602.72 | 55-64 |
| 31249 | Male | Food and Beverage | 2 | 10.46 | Cash | 7/10/2021 | Metrocity | 20.92 | 35-44 |
Kiểm tra số lượng giao dịch trùng lặp sau khi loại bỏ hai cột trên.
transactions.duplicated().sum()
1111
transactions.drop_duplicates(keep='first')
| gender | category | quantity | price | payment_method | invoice_date | shopping_mall | total | age_group | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | Female | Clothing | 5 | 1500.40 | Credit Card | 5/8/2022 | Kanyon | 7502.00 | 25-34 |
| 1 | Male | Shoes | 3 | 1800.51 | Debit Card | 12/12/2021 | Forum Istanbul | 5401.53 | 18-24 |
| 2 | Male | Clothing | 1 | 300.08 | Cash | 9/11/2021 | Metrocity | 300.08 | 18-24 |
| 3 | Female | Shoes | 5 | 3000.85 | Credit Card | 16/05/2021 | Metropol AVM | 15004.25 | 65-70 |
| 4 | Female | Books | 4 | 60.60 | Cash | 24/10/2021 | Kanyon | 242.40 | 45-54 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 99452 | Female | Souvenir | 5 | 58.65 | Credit Card | 21/09/2022 | Kanyon | 293.25 | 45-54 |
| 99453 | Male | Food and Beverage | 2 | 10.46 | Cash | 22/09/2021 | Forum Istanbul | 20.92 | 25-34 |
| 99454 | Male | Food and Beverage | 2 | 10.46 | Debit Card | 28/03/2021 | Metrocity | 20.92 | 55-64 |
| 99455 | Male | Technology | 4 | 4200.00 | Cash | 16/03/2021 | Istinye Park | 16800.00 | 55-64 |
| 99456 | Female | Souvenir | 3 | 35.19 | Credit Card | 15/10/2022 | Mall of Istanbul | 105.57 | 35-44 |
98346 rows × 9 columns
Trước khi thực hiện việc khai phá dữ liệu, nhóm sẽ thực hiện phân tích sơ bộ tập dữ liệu hiện tại thông qua biểu đồ trực quan để hiểu hơn về nghiệp vụ trước khi thực hiện khai phá.
import seaborn as sns
import plotly.express as px
Đầu tiên, danh mục sản phẩm phổ biến nhất trên tổng số lượng sản phẩm trong mỗi giao dịch.
category = transactions.groupby('category')['quantity'].sum()
category = pd.DataFrame({'category': category.index, 'quantity': category.values})
category['categories'] = 'categories'
fig = px.treemap(category, path=['categories', 'category'], values='quantity', color='quantity',
hover_data=['category'], color_continuous_scale='Blues')
fig.update_layout(width=1000, height=600, paper_bgcolor='LightSteelBlue')
fig.show(renderer='notebook')
Như vậy, sản phẩm thuộc danh mục Clothing, Cosmetics, và Food and Beverage xuất hiện nhiều nhất trong toàn bộ số giao dịch.
Đáng lưu ý, Clothing và Cosmetics là hai danh mục sản phẩm trên thực tế thường được mua bởi phụ nữ, nên có thể số lượng khách hàng nữ cao hơn nam.
transactions['gender'].value_counts()
Female 59482 Male 39975 Name: gender, dtype: int64
Với số lượng khách hàng nữ cao hơn gần 20000, doanh thu có thể phần lớn đến từ khách hàng nữ.
gender = transactions.groupby('gender')['total'].sum()
gender = pd.DataFrame({'gender': gender.index, 'total': gender.values})
fig = px.pie(gender, values='total', names='gender')
fig.update_layout(paper_bgcolor='LightSteelBlue')
fig.show(renderer='notebook')
Đúng như dự đoán, gần 60% doanh thu đến từ khách hàng nữ.
gender_category = transactions.groupby(['gender', 'category'])['total'].sum().unstack().reset_index()
fig = px.bar(gender_category,
x=['Books', 'Clothing', 'Cosmetics', 'Food and Beverage', 'Shoes', 'Souvenir', 'Technology', 'Toys'],
y='gender')
fig.update_layout(width=1000, height=600, plot_bgcolor='LightSteelBlue', paper_bgcolor='LightSteelBlue',
legend=dict(title='category'))
fig.show(renderer='notebook')
Với mỗi danh mục sản phẩm, khách hàng nữ đều chi nhiều hơn khách hàng nam khi mua sắm. Tuy nhiên, đây cũng có thể là vì số lượng khách hàng nữ cao hơn. Vì vậy, nhóm không thể dựa vào biểu đồ trực quan như trên để đưa ra quyết định nghiệp vụ marketing hoặc xây dựng hệ thống recommendation. Thay vào đó, để đưa ra chiến lược nhằm duy trì mối quan hệ khách hàng chính xác và hiệu quả, nhóm cần thực hiện quá trình khai phá dữ liệu.
Mục tiêu chính của nhóm là xác định phân khúc khách hàng thân thiết hoặc sản phẩm có giá trị doanh nghiệp cao dựa trên thuật toán phân cụm (Clustering) và phân loại (Classification). Ngoài ra, thuật toán kết hợp (Associate) cũng sẽ được sử dụng để phân tích hành vi mua hàng của khách hàng và xu hướng, khuôn mẫu có ích cho quyết định nghiệp vụ.
Phân loại là quá trình gồm hai bước: learning và predicting. Trong bước learning, mô hình phân loại được hình thành sử dụng tập dữ liệu training. Trong bước predicting, mô hình trên sẽ được sử dụng để đưa ra dự đoán dựa trên đầu vào. Phân loại, khác với phân cụm, là mô hình học máy có giám sát. Nhóm sẽ sử dụng thuật toán Decision Tree vì thuật toán dễ trực quan hóa và dễ hiểu.
from sklearn import preprocessing
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import train_test_split
from sklearn import metrics
from sklearn.model_selection import GridSearchCV
from sklearn.tree import export_graphviz
from six import StringIO
from IPython.display import Image
import pydotplus
pd.options.mode.chained_assignment = None
Do Decision Tree là mô hình học máy có giám sát, nhóm sẽ xác định biến giải thích (feature variables) và biến kết quả (target variables) trong nghiệp vụ phân loại giới tính khách hàng.
features = ['age_group', 'category', 'quantity', 'payment_method', 'total']
targets = ['gender']
X = transactions[features]
y = transactions[targets]
Với hệ thống học máy có nền tảng mạnh, cột có kiểu phân loại được xử lý một cách tự nhiên như ngôn ngữ R sẽ sử dụng factors, hoặc Weka sẽ sử dụng kiểu nominal. Mô hình Decision Tree nhóm sử dụng từ thư viện scikit-learn chỉ chấp nhận biến giải thích (feature variables) kiểu số và liên tục (continuous numerical variables). Để chuyển đổi kiểu dữ liệu, nhóm có hai lựa chọn: one-hot-encoding và label-encoding. Tuy nhiên, khi sử dụng label-encoding trên một cột, mô hình học máy có thể vô tình xem cột đó có thứ tự hoặc cấp bậc. Nhóm có thể mong muốn việc này với cột độ tuổi, tuy nhiên, cột danh mục sản phẩm và phương thức thanh toán không nên có. Sử dụng label-encoding trên cột độ tuổi.
label_encoder = preprocessing.LabelEncoder()
label_encoder.fit(X.age_group)
label_encoder.classes_
array(['18-24', '25-34', '35-44', '45-54', '55-64', '65-70', nan],
dtype=object)
Thay thế cột độ tuổi ban đầu.
X['age_group'] = label_encoder.fit_transform(X['age_group'])
X.sample(5)
| age_group | category | quantity | payment_method | total | |
|---|---|---|---|---|---|
| 13392 | 4 | Clothing | 5 | Cash | 7502.00 |
| 37302 | 2 | Clothing | 4 | Cash | 4801.28 |
| 98365 | 4 | Food and Beverage | 4 | Cash | 83.68 |
| 7829 | 3 | Clothing | 4 | Debit Card | 4801.28 |
| 47253 | 1 | Books | 1 | Cash | 15.15 |
Sử dụng one-hot-encoding trên cột danh mục sản phẩm và phương thức thanh toán.
X = pd.get_dummies(X, columns=['category', 'payment_method'])
X.sample(5)
| age_group | quantity | total | category_Books | category_Clothing | category_Cosmetics | category_Food and Beverage | category_Shoes | category_Souvenir | category_Technology | category_Toys | payment_method_Cash | payment_method_Credit Card | payment_method_Debit Card | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 66912 | 0 | 1 | 40.66 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
| 89263 | 6 | 5 | 293.25 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 0 |
| 2415 | 0 | 4 | 16800.00 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 0 |
| 949 | 0 | 3 | 365.94 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
| 82810 | 4 | 3 | 2700.72 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
Ngoài ra, biến kết quả (target variables) giới tính cũng nên được áp dụng label-encoding.
label_encoder = preprocessing.LabelEncoder()
label_encoder.fit(y.gender)
label_encoder.classes_
array(['Female', 'Male'], dtype=object)
Thay thế cột giới tính ban đầu.
y['gender'] = label_encoder.fit_transform(y['gender'])
y.sample(5)
| gender | |
|---|---|
| 44695 | 1 |
| 22231 | 0 |
| 36260 | 1 |
| 48147 | 1 |
| 62241 | 0 |
Để xét độ chính xác của mô hình, nhóm sẽ chia tập dữ liệu thành tập dữ liệu dành cho training và tập dữ liệu dành cho testing. Nhóm sẽ dành ra 70% giao dịch từ tập dữ liệu ban đầu cho việc training và 30% cho việc testing.
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=1, stratify=y)
Bắt đầu với việc fit tập dữ liệu training vào mô hình Decision Tree.
clf = DecisionTreeClassifier()
clf = clf.fit(X_train, y_train)
Tiếp theo, dự đoán biến kết quả (target variables) với đầu vào là tập dữ liệu testing X chứa biến giải thích (feature variables).
y_pred = clf.predict(X_test)
y_pred[:5]
array([0, 0, 0, 0, 0])
Xét độ chính xác của mô hình bằng phương pháp so sánh giữa tập dữ liệu testing y chứa biến kết quả (target variables) và tập dữ liệu dự đoán trên.
metrics.accuracy_score(y_test, y_pred)
0.5924324686641196
Hyper-parameters là tham số có thể định nghĩa lúc xây dựng mô hình học máy. Với Decision Tree, việc cấu hình quy luật thuật toán phân chia dữ liệu (theo entropy hay gini impurity) hoặc chiều sâu tối đa có thể giúp tăng độ chính xác của mô hình và tránh overfitting. Việc tìm ra tổ hợp tham số tốt nhất cho mô hình có thể được tự động hóa sử dụng GridSearchCV. Đầu tiên, nhóm sẽ xác định tham số nhóm muốn thay đổi. GridSearchCV thực hiện cross-validation đối với từng tổ hợp tham số trên và xác định tổ hợp tham số tốt nhất.
params = {
'criterion': ['gini', 'entropy'],
'max_depth': [None, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
'max_features': [None, 'sqrt', 'log2', 0.2, 0.4, 0.6, 0.8] + list(range(1, 10)),
'splitter': ['best', 'random']
}
clf = GridSearchCV(estimator=DecisionTreeClassifier(), param_grid=params, cv=5, n_jobs=-1, verbose=1)
clf.fit(X_train, y_train)
clf.best_params_
Fitting 5 folds for each of 704 candidates, totalling 3520 fits
{'criterion': 'gini',
'max_depth': 5,
'max_features': 0.2,
'splitter': 'random'}
Sử dụng tổ hợp tham số tốt nhất GridSearchCV tìm được để xây dựng lại mô hình.
clf = DecisionTreeClassifier(criterion=clf.best_params_['criterion'], splitter=clf.best_params_['splitter'],
max_depth=clf.best_params_['max_depth'], max_features=clf.best_params_['max_features'])
clf = clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
metrics.accuracy_score(y_test, y_pred)
0.5981634157785375
Với tổ hợp tham số mới, độ chính xác của mô hình tăng nhẹ và mô hình không còn bị overfitting.
Biểu đồ trực quan Decision Tree cho thấy cấu trúc mô hình học máy với mỗi ô chữ nhật là một nút. Nội dung một nút cho biết quy luật thuật toán phân chia dữ liệu tại dòng đầu tiên và biến kết quả (target variables) tại dòng cuối cùng.
dot_data = StringIO()
export_graphviz(clf, out_file=dot_data, filled=True, rounded=True, special_characters=True, feature_names=X.columns,
class_names=['0', '1'])
graph = pydotplus.graph_from_dot_data(dot_data.getvalue())
graph.write_png('gender.png')
dot_data = StringIO()
Image(graph.create_png())